Skip to content

improvement: Rework IndexedContext to reuse the previously calculated scopes #22898

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

tgodzik
Copy link
Contributor

@tgodzik tgodzik commented Apr 1, 2025

It turns out the work being done in IndexedContext was already done in Completions, but better, since it doesn't try to read files as the separate logic does.

There is still some improvement to be done to not calculate it twice, but in order to keep this PR as simple as possible I will skip that for now.

@som-snytt
Copy link
Contributor

som-snytt commented Apr 1, 2025

I wonder if this strategy helps #22430 (which I haven't returned to yet)

Edit: I guess only in the sense of "moral support".

@tgodzik
Copy link
Contributor Author

tgodzik commented Apr 1, 2025

I think I am actually doing it all wrong and might be possible that IndexedContext is not really needed 😅

@tgodzik tgodzik force-pushed the fix-slow branch 2 times, most recently from 3e69d9e to 21aa3b8 Compare April 3, 2025 18:27
@tgodzik tgodzik force-pushed the fix-slow branch 4 times, most recently from d27ca2c to 376f628 Compare April 16, 2025 20:05
@tgodzik tgodzik changed the title improvement: Look up isAccessible lazily improvement: Rework IndexedContext to reuse the previously calculated scopes Apr 16, 2025
… scopes

It turns out the work being done in IndexedContext was already done in Completions, but better, since it doesn't try to read files as the separate logic does.

There is still some improvement to be done to not calculate it twice, but in order to keep this PR as simple as possible I will skip that for now.
@@ -48,6 +48,16 @@ case class Completion(label: String, description: String, symbols: List[Symbol])

object Completion:

def scopeContext(pos: SourcePosition)(using Context): CompletionResult =
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally this would get deduplicated with normal rawCompletions being invoked, but I will try to do it as a separate step afterwards.

@Test def `sealed-conflict` =
check(
"""
|object A {
| val e: Either[Int, String] = ???
| type Left = String
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type val actually fine here, but val causes an error

@@ -655,7 +655,7 @@ class InlayHintsSuite extends BaseInlayHintsSuite {
| val y/*: S<<scala/collection/Set#>>[Char<<scala/Char#>>]*/ = f
| ???
| }
| val x/*: AB<<scala/collection/AbstractMap#>>[Int<<scala/Int#>>, String<<scala/Predef.String#>>]*/ = test(Set/*[Int<<scala/Int#>>]*/(1), Set/*[Char<<scala/Char#>>]*/('a'))
| val x/*: AB<<scala/collection/AbstractMap#>>[Int<<scala/Int#>>, String<<java/lang/String#>>]*/ = test(Set/*[Int<<scala/Int#>>]*/(1), Set/*[Char<<scala/Char#>>]*/('a'))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was inconsistent previously

@tgodzik tgodzik requested review from rochala and kasiaMarek April 17, 2025 10:18
@tgodzik tgodzik marked this pull request as ready for review April 17, 2025 10:18
Copy link
Member

@kasiaMarek kasiaMarek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some nitpicks and questions, but overall looks really cool 🎉

Comment on lines +100 to +107
val fromPredef = defn.ScalaPredefModuleClass.membersNamed(name)
val fromScala = defn.ScalaPackageClass.membersNamed(name)
val fromJava = defn.JavaLangPackageClass.membersNamed(name)
val predefList = if fromPredef.exists then List(fromPredef.first.symbol) else Nil
val scalaList = if fromScala.exists then List(fromScala.first.symbol) else Nil
val javaList = if fromJava.exists then List(fromJava.first.symbol) else Nil
val combined = predefList ++ scalaList ++ javaList
if combined.nonEmpty then Some(combined) else None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
val fromPredef = defn.ScalaPredefModuleClass.membersNamed(name)
val fromScala = defn.ScalaPackageClass.membersNamed(name)
val fromJava = defn.JavaLangPackageClass.membersNamed(name)
val predefList = if fromPredef.exists then List(fromPredef.first.symbol) else Nil
val scalaList = if fromScala.exists then List(fromScala.first.symbol) else Nil
val javaList = if fromJava.exists then List(fromJava.first.symbol) else Nil
val combined = predefList ++ scalaList ++ javaList
if combined.nonEmpty then Some(combined) else None
List(defn.ScalaPredefModuleClass, defn.ScalaPackageClass, defn.JavaLangPackageClass)
.map(_.membersNamed(name))
.collect { case denot if denot.exists => denot.first.symbol }
.toList match
case Nil => None
case list => Some(list)

Comment on lines +90 to +91
def hasImportedByDefault = denots.exists(denot => Interactive.isImportedByDefault(denot.symbol))
def hasConflictingValue = denots.exists(denot => !Interactive.isImportedByDefault(denot.symbol))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe make sense to use partition here? If you can do this on nonRoot instead of denots you could return the created collection.

case Some(symbols) if symbols.nonEmpty && symbols.forall(_.isStale) => Result.Missing
case Some(symbols) if symbols.exists(rename(_).isEmpty) => Result.Conflict
case Some(symbols) if symbols.exists(rename(_).isEmpty) && rename(sym).isEmpty => Result.Conflict
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't here some sort of corner case?

package a
class Foo

package b
class Foo

package c
class Foo

package d
import a.{Foo => Bar}
import b.Foo

val x = new Foo // I want to import c.Foo
// for `b.Foo` rename will be empty, so I won't fall into this case

@@ -138,44 +141,40 @@ object NamedArgCompletions:
def fallbackFindMatchingMethods() =
def maybeNameAndIndexedContext(
method: Tree
): Option[(Name, IndexedContext)] =
): List[Symbol] =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you rename this function after changes?

if !id.symbol.is(Synthetic) && !id.symbol.is(Implicit) =>
symbols + tree.symbol
case sel: Select =>
indexedContext.lookupSym(sel.symbol) match
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm questioning this right now. Shouldn't we actually just collect the symbol of the qualifier? (Sort of a side comment not connected to the PR)

@tailrec
final def toplevelClashes(sym: Symbol): Boolean =
final def toplevelClashes(sym: Symbol, inImportScope: Boolean): Boolean =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really get this inImportScope param. Why did you add a symbols.exists(_.owner.is(Package) check? Wouldn't something like pkg.pk2.Object.name also conflict with package name?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants